/*
 * Phys2D - a 2D physics engine based on the work of Erin Catto. The
 * original source remains:
 * 
 * Copyright (c) 2006 Erin Catto http://www.gphysics.com
 * 
 * This source is provided under the terms of the BSD License.
 * 
 * Copyright (c) 2006, Phys2D
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or 
 * without modification, are permitted provided that the following 
 * conditions are met:
 * 
 *  * Redistributions of source code must retain the above 
 *    copyright notice, this list of conditions and the 
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above 
 *    copyright notice, this list of conditions and the following 
 *    disclaimer in the documentation and/or other materials provided 
 *    with the distribution.
 *  * Neither the name of the Phys2D/New Dawn Software nor the names of 
 *    its contributors may be used to endorse or promote products 
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 
 * OF SUCH DAMAGE.
 */
package com.phys2d.demo.client;

import com.google.gwt.dom.client.Document;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.user.client.Timer;

import net.phys2d.client.math.ROVector2f;
import net.phys2d.client.math.Vector2f;
import net.phys2d.client.raw.Body;
import net.phys2d.client.raw.BodyList;
import net.phys2d.client.raw.World;
import net.phys2d.client.raw.shapes.Box;
import net.phys2d.client.raw.strategies.QuadSpaceStrategy;
import gwt.g2d.client.graphics.KnownColor;
import gwt.g2d.client.graphics.Surface;
import gwt.g2d.client.graphics.shapes.ShapeBuilder;

/**
 * A common demo box super class.
 * 
 * @author Kevin Glass
 */
public abstract class GwtDemo {

	/** The title of the current demo */
	protected String title;
	/** The world containing the physics model */
	protected World world = new World(new Vector2f(0.0f, 10.0f), 10,
			new QuadSpaceStrategy(20, 5));
	/** True if the simulation is running */
	//private boolean running = true;

	/** gwt-g2d canvas surface */
	private final Surface surface;

	/** counter for click-generated bodies */
	private int clickBodyCounter = 0;
	
	/** True if we should reset the demo on the next loop */
	protected boolean needsReset;
	/** True if we should render normals */
	private boolean normals = true;
	/** True if we should render contact points */
	private boolean contacts = true;
	
	/** x position of last mouse down-click */
	private int xDown = 0;
	/** x position of last mouse down-click */
	private int yDown = 0;

	/**
	 * Create a new demo
	 * 
	 * @param title
	 *            The title of the demo
	 */
	public GwtDemo(String title, Surface surface) {
		this.title = title;
		this.surface = surface;
	}

	/**
	 * Retrieve the title of the demo
	 * 
	 * @return The title of the demo
	 */
	public String getTitle() {
		return title;
	}

	/**
	 * Notification that a key was pressed
	 * 
	 * @param c
	 *            The character of key hit
	 */
	protected void keyHit(char c) {
		if (c == 'r') {
			needsReset = true;
		}
		if (c == 'c') {
			normals = !normals;
			contacts = !contacts;
		}
	}

	/**
	 * Initialise the GUI
	 */
	private void initGUI() {
		// NOTE: from GWT docs, using anonymous inner classes for this may result in excess memory usage... 
		surface.addMouseDownHandler(new MouseDownHandler (){
			public void onMouseDown(MouseDownEvent event) {
				int xCur = event.getNativeEvent().getClientX()- surface.getAbsoluteLeft() + Document.get().getScrollLeft();
				int yCur = event.getNativeEvent().getClientY()- surface.getAbsoluteTop() + Document.get().getScrollTop();
				
				xDown = xCur;
				yDown = yCur;
			}
		});
		
		surface.addMouseUpHandler(new MouseUpHandler(){
			public void onMouseUp(MouseUpEvent event) {
				int xCur = event.getNativeEvent().getClientX()- surface.getAbsoluteLeft() + Document.get().getScrollLeft();
				int yCur = event.getNativeEvent().getClientY()- surface.getAbsoluteTop() + Document.get().getScrollTop();
				int xLeft = Math.min(xCur, xDown);
				int yTop = Math.min(yCur, yDown);
				float boxDensity = 0.5f;
				int boxWidth = 30;
				int boxHeight = 30;
				if (xCur != xDown || yCur != yDown)	{
					boxWidth = Math.abs(xCur - xDown);
					boxHeight = Math.abs(yCur - yDown);
				}
				Body clickBody = new Body("ClickBody " + clickBodyCounter, 
						new Box(boxWidth, boxHeight), boxWidth*boxHeight*boxDensity);
				clickBody.setPosition(xLeft+boxWidth/2, yTop+boxHeight/2);
				world.add(clickBody);
			}
		});
	}

	/**
	 * Start the simulation running
	 */
	public void start() {
		initGUI();
		initDemo();

		// float target = 1000 / 60.0f;
		// float frameAverage = target;
		// long lastFrame = System.currentTimeMillis();
		// float yield = 10000f;
		// float damping = 0.1f;

		// long renderTime = 0;
		// long logicTime = 0;

		Timer updateTimer = new Timer() {

			public void run() {
				update();
			}
		};
		updateTimer.scheduleRepeating(50);
		// update();
	}

	/**
	 * Update the demo - just in case we want to add anything over the top
	 */
	protected void update() {
		// adaptive timing loop from Master Onyx
		//long timeNow = System.currentTimeMillis();
		// frameAverage = (frameAverage * 10 + (timeNow - lastFrame)) / 11;
		// lastFrame = timeNow;

		// yield+=yield*((target/frameAverage)-1)*damping+0.05f;
		//
		// for(int i=0;i<yield;i++) {
		// Thread.yield();
		// }

		// render
		//long beforeRender = System.currentTimeMillis();
		// Graphics2D g = (Graphics2D) strategy.getDrawGraphics();
		// g.setColor(Color.white);
		// g.fillRect(0,0,500,500);
		//		
		// draw(g);
		// renderGUI(g);
		// g.setColor(Color.black);
		// g.drawString("FAv: "+frameAverage,10,50);
		// g.drawString("FPS: "+(int) (1000 / frameAverage),10,70);
		// g.drawString("Yield: "+yield,10,90);
		// g.drawString("Arbiters: "+world.getArbiters().size(),10,110);
		// g.drawString("Bodies: "+world.getBodies().size(),10,130);
		// g.drawString("R: "+renderTime,10,150);
		// g.drawString("L: "+logicTime,10,170);
		// g.drawString("Energy: "+world.getTotalEnergy(),10,190);
		// g.dispose();
		// strategy.show();

		// some temporary code for outputting body values
		BodyList bodies = world.getBodies();

		// refresh canvas
		surface.fillBackground(KnownColor.LIGHT_BLUE);
		
		for (int i = 0; i < bodies.size(); i++) {
			Body body = bodies.get(i);

			// System.out.println(body);
			ROVector2f pos = body.getPosition();
			surface.save().setFillStyle(KnownColor.GREEN_YELLOW).fillShape(
					new ShapeBuilder().drawCircle(pos.getX(), pos.getY(), 5)
							.build()).restore();
			drawBody(body);
		}

		// renderTime = System.currentTimeMillis() - beforeRender;

		// update data model
		// long beforeLogic = System.currentTimeMillis();
		for (int i = 0; i < 5; i++) {
			world.step();
		}
		// logicTime = System.currentTimeMillis() - beforeLogic;

		if (needsReset) {
			world.clear();
			initDemo();
			needsReset = false;
			// frameAverage = target;
			// yield = 10000f;
		}

	}

	/**
	 * Demo customisable GUI render
	 * 
	 * @param g
	 *            The graphics context to use for rendering here
	 */
	// protected void renderGUI(Graphics2D g) {
	// g.setColor(Color.black);
	// g.drawString("R - Restart Demo",15,430);
	// }

	// /**
	// * Draw a specific contact point determined from the simulation
	// *
	// * @param g The graphics context on which to draw
	// * @param contact The contact to draw
	// */
	// protected void drawContact(Graphics2D g, Contact contact) {
	// int x = (int) contact.getPosition().getX();
	// int y = (int) contact.getPosition().getY();
	// if (contacts) {
	// g.setColor(Color.blue);
	// g.fillOval(x-3,y-3,6,6);
	// }
	//		
	// if (normals) {
	// int dx = (int) (contact.getNormal().getX() * 10);
	// int dy = (int) (contact.getNormal().getY() * 10);
	// g.setColor(Color.darkGray);
	// g.drawLine(x,y,x+dx,y+dy);
	// }
	// }
	//	
	 /**
	 * Draw a body
	 * @param body The body to be drawn
	 */
	 protected void drawBody(Body body) {
	 if (body.getShape() instanceof Box) {
	 drawBoxBody(body,(Box) body.getShape());
	 }
	// if (body.getShape() instanceof Circle) {
	// drawCircleBody(g,body,(Circle) body.getShape());
	// }
	// if (body.getShape() instanceof Line) {
	// drawLineBody(g,body,(Line) body.getShape());
	// }
	// if (body.getShape() instanceof Polygon) {
	// drawPolygonBody(g,body,(Polygon) body.getShape());
	 }
//	 }
		
	// /**
	// * Draw a polygon into the demo
	// *
	// * @param g The graphics to draw the poly onto
	// * @param body The body describing the poly's position
	// * @param poly The poly to be drawn
	// */
	// protected void drawPolygonBody(Graphics2D g, Body body, Polygon poly) {
	// g.setColor(Color.black);
	//
	// ROVector2f[] verts = poly.getVertices(body.getPosition(),
	// body.getRotation());
	// for ( int i = 0, j = verts.length-1; i < verts.length; j = i, i++ ) {
	// g.drawLine(
	// (int) (0.5f + verts[i].getX()),
	// (int) (0.5f + verts[i].getY()),
	// (int) (0.5f + verts[j].getX()),
	// (int) (0.5f + verts[j].getY()));
	// }
	// }
	//
	// /**
	// * Draw a line into the demo
	// *
	// * @param g The graphics to draw the line onto
	// * @param body The body describing the line's position
	// * @param line The line to be drawn
	// */
	// protected void drawLineBody(Graphics2D g, Body body, Line line) {
	// g.setColor(Color.black);
	// //
	// // float x = body.getPosition().getX();
	// // float y = body.getPosition().getY();
	// // float dx = line.getDX();
	// // float dy = line.getDY();
	// //
	// // g.drawLine((int) x,(int) y,(int) (x+dx),(int) (y+dy));
	// Vector2f[] verts = line.getVertices(body.getPosition(),
	// body.getRotation());
	// g.drawLine(
	// (int) verts[0].getX(),
	// (int) verts[0].getY(),
	// (int) verts[1].getX(),
	// (int) verts[1].getY());
	// }
	//	
	// /**
	// * Draw a circle in the world
	// *
	// * @param g The graphics contact on which to draw
	// * @param body The body to be drawn
	// * @param circle The shape to be drawn
	// */
	// protected void drawCircleBody(Graphics2D g, Body body, Circle circle) {
	// g.setColor(Color.black);
	// float x = body.getPosition().getX();
	// float y = body.getPosition().getY();
	// float r = circle.getRadius();
	// float rot = body.getRotation();
	// float xo = (float) (Math.cos(rot) * r);
	// float yo = (float) (Math.sin(rot) * r);
	//		
	// g.drawOval((int) (x-r),(int) (y-r),(int) (r*2),(int) (r*2));
	// g.drawLine((int) x,(int) y,(int) (x+xo),(int) (y+yo));
	// }
	//	

	/**
	 * Draw a box in the world
	 * 
	 * @param body
	 *            The body to be drawn
	 * @param box
	 *            The shape to be drawn
	 */
	protected void drawBoxBody(Body body, Box box) {
		Vector2f[] pts = box.getPoints(body.getPosition(), body.getRotation());

		Vector2f v1 = pts[0];
		Vector2f v2 = pts[1];
		Vector2f v3 = pts[2];
		Vector2f v4 = pts[3];

//		g.setColor(Color.black);
//		g.drawLine((int) v1.x, (int) v1.y, (int) v2.x, (int) v2.y);
//		g.drawLine((int) v2.x, (int) v2.y, (int) v3.x, (int) v3.y);
//		g.drawLine((int) v3.x, (int) v3.y, (int) v4.x, (int) v4.y);
//		g.drawLine((int) v4.x, (int) v4.y, (int) v1.x, (int) v1.y);
		
		surface.save()
			.setFillStyle(KnownColor.DARK_BLUE)
			.strokeShape(new ShapeBuilder()
				.drawLineSegment(v1.x, v1.y, v2.x, v2.y)
				.drawLineSegment(v2.x, v2.y, v3.x, v3.y)
				.drawLineSegment(v3.x, v3.y, v4.x, v4.y)
				.drawLineSegment(v4.x, v4.y, v1.x, v1.y)
				.build())
			.fillShape(new ShapeBuilder()
				.drawCircle(v1.x, v1.y, 2)
				.drawCircle(v2.x, v2.y, 2)
				.drawCircle(v3.x, v3.y, 2)
				.drawCircle(v4.x, v4.y, 2)
				.build())
			.restore();
	}
	
	// /**
	// * Draw a joint
	// *
	// * @param g The graphics contact on which to draw
	// * @param j The joint to be drawn
	// */
	// public void drawJoint(Graphics2D g, Joint j) {
	// if (j instanceof FixedJoint) {
	// FixedJoint joint = (FixedJoint) j;
	//			
	// g.setColor(Color.red);
	// float x1 = joint.getBody1().getPosition().getX();
	// float x2 = joint.getBody2().getPosition().getX();
	// float y1 = joint.getBody1().getPosition().getY();
	// float y2 = joint.getBody2().getPosition().getY();
	//			
	// g.drawLine((int) x1,(int) y1,(int) x2,(int) y2);
	// }
	// if(j instanceof SlideJoint){
	// SlideJoint joint = (SlideJoint) j;
	//			
	// Body b1 = joint.getBody1();
	// Body b2 = joint.getBody2();
	//	
	// Matrix2f R1 = new Matrix2f(b1.getRotation());
	// Matrix2f R2 = new Matrix2f(b2.getRotation());
	//	
	// ROVector2f x1 = b1.getPosition();
	// Vector2f p1 = MathUtil.mul(R1,joint.getAnchor1());
	// p1.add(x1);
	//	
	// ROVector2f x2 = b2.getPosition();
	// Vector2f p2 = MathUtil.mul(R2,joint.getAnchor2());
	// p2.add(x2);
	//			
	// Vector2f im = new Vector2f(p2);
	// im.sub(p1);
	// im.normalise();
	//			
	//			
	//			
	// g.setColor(Color.red);
	// g.drawLine((int)p1.x,(int)p1.y,(int)(p1.x+im.x*joint.getMinDistance()),(int)(p1.y+im.y*joint.getMinDistance()));
	// g.setColor(Color.blue);
	// g.drawLine((int)(p1.x+im.x*joint.getMinDistance()),(int)(p1.y+im.y*joint.getMinDistance()),(int)(p1.x+im.x*joint.getMaxDistance()),(int)(p1.y+im.y*joint.getMaxDistance()));
	// }
	// if(j instanceof AngleJoint){
	// AngleJoint angleJoint = (AngleJoint)j;
	// Body b1 = angleJoint.getBody1();
	// Body b2 = angleJoint.getBody2();
	// float RA = j.getBody1().getRotation() + angleJoint.getRotateA();
	// float RB = j.getBody1().getRotation() + angleJoint.getRotateB();
	//			
	// Vector2f VA = new Vector2f((float) Math.cos(RA), (float) Math.sin(RA));
	// Vector2f VB = new Vector2f((float) Math.cos(RB), (float) Math.sin(RB));
	//			
	// Matrix2f R1 = new Matrix2f(b1.getRotation());
	// Matrix2f R2 = new Matrix2f(b2.getRotation());
	//			
	// ROVector2f x1 = b1.getPosition();
	// Vector2f p1 = MathUtil.mul(R1,angleJoint.getAnchor1());
	// p1.add(x1);
	//	
	// ROVector2f x2 = b2.getPosition();
	// Vector2f p2 = MathUtil.mul(R2,angleJoint.getAnchor2());
	// p2.add(x2);
	//			
	// g.setColor(Color.red);
	// g.drawLine((int)p1.x,(int)p1.y,(int)(p1.x+VA.x*20),(int)(p1.y+VA.y*20));
	// g.drawLine((int)p1.x,(int)p1.y,(int)(p1.x+VB.x*20),(int)(p1.y+VB.y*20));
	// }
	// if (j instanceof BasicJoint) {
	// BasicJoint joint = (BasicJoint) j;
	//			
	// Body b1 = joint.getBody1();
	// Body b2 = joint.getBody2();
	//	
	// Matrix2f R1 = new Matrix2f(b1.getRotation());
	// Matrix2f R2 = new Matrix2f(b2.getRotation());
	//	
	// ROVector2f x1 = b1.getPosition();
	// Vector2f p1 = MathUtil.mul(R1,joint.getLocalAnchor1());
	// p1.add(x1);
	//	
	// ROVector2f x2 = b2.getPosition();
	// Vector2f p2 = MathUtil.mul(R2,joint.getLocalAnchor2());
	// p2.add(x2);
	//	
	// g.setColor(Color.red);
	// g.drawLine((int) x1.getX(), (int) x1.getY(), (int) p1.x, (int) p1.y);
	// g.drawLine((int) p1.x, (int) p1.y, (int) x2.getX(), (int) x2.getY());
	// g.drawLine((int) x2.getX(), (int) x2.getY(), (int) p2.x, (int) p2.y);
	// g.drawLine((int) p2.x, (int) p2.y, (int) x1.getX(), (int) x1.getY());
	// }
	// if(j instanceof DistanceJoint){
	// DistanceJoint joint = (DistanceJoint) j;
	//			
	// Body b1 = joint.getBody1();
	// Body b2 = joint.getBody2();
	//	
	// Matrix2f R1 = new Matrix2f(b1.getRotation());
	// Matrix2f R2 = new Matrix2f(b2.getRotation());
	//	
	// ROVector2f x1 = b1.getPosition();
	// Vector2f p1 = MathUtil.mul(R1,joint.getAnchor1());
	// p1.add(x1);
	//	
	// ROVector2f x2 = b2.getPosition();
	// Vector2f p2 = MathUtil.mul(R2,joint.getAnchor2());
	// p2.add(x2);
	//			
	// g.setColor(Color.red);
	// g.drawLine((int) p1.getX(), (int) p1.getY(), (int) p2.x, (int) p2.y);
	// }
	// if (j instanceof SpringJoint) {
	// SpringJoint joint = (SpringJoint) j;
	//			
	// Body b1 = joint.getBody1();
	// Body b2 = joint.getBody2();
	//	
	// Matrix2f R1 = new Matrix2f(b1.getRotation());
	// Matrix2f R2 = new Matrix2f(b2.getRotation());
	//	
	// ROVector2f x1 = b1.getPosition();
	// Vector2f p1 = MathUtil.mul(R1,joint.getLocalAnchor1());
	// p1.add(x1);
	//	
	// ROVector2f x2 = b2.getPosition();
	// Vector2f p2 = MathUtil.mul(R2,joint.getLocalAnchor2());
	// p2.add(x2);
	//			
	// g.setColor(Color.red);
	// g.drawLine((int) x1.getX(), (int) x1.getY(), (int) p1.x, (int) p1.y);
	// g.drawLine((int) p1.x, (int) p1.y, (int) p2.getX(), (int) p2.getY());
	// g.drawLine((int) p2.getX(), (int) p2.getY(), (int) x2.getX(), (int)
	// x2.getY());
	// }
	// }
	//	
	// /**
	// * Draw the whole simulation
	// *
	// * @param g The graphics context on which to draw
	// */
	// protected void draw(Graphics2D g) {
	// BodyList bodies = world.getBodies();
	//		
	// for (int i=0;i<bodies.size();i++) {
	// Body body = bodies.get(i);
	//			
	// drawBody(g, body);
	// }
	//		
	// JointList joints = world.getJoints();
	//		
	// for (int i=0;i<joints.size();i++) {
	// Joint joint = joints.get(i);
	//			
	// drawJoint(g, joint);
	// }
	//		
	// ArbiterList arbs = world.getArbiters();
	//		
	// for (int i=0;i<arbs.size();i++) {
	// Arbiter arb = arbs.get(i);
	//			
	// Contact[] contacts = arb.getContacts();
	// int numContacts = arb.getNumContacts();
	//			
	// for (int j=0;j<numContacts;j++) {
	// drawContact(g, contacts[j]);
	// }
	// }
	// }

	/**
	 * Initialise the demo - clear the world
	 */
	public final void initDemo() {
		world.clear();
		world.setGravity(0, 10);

		System.out.println("Initialising:" + getTitle());
		init(world);
	}

	/**
	 * Should be implemented by the demo, add the bodies/joints to the world.
	 * 
	 * @param world
	 *            The world in which the simulation is going to run
	 */
	protected abstract void init(World world);

}
